# Copyright 2016 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# WARNING:
# https://github.com/bazelbuild/bazel/issues/17713
# .bzl files in this package (tools/build_defs/repo) are evaluated
# in a Starlark environment without "@_builtins" injection, and must not refer
# to symbols associated with build/workspace .bzl files

# Implementations of Maven rules in Starlark:
# 1) maven_jar(name, artifact, repository, sha1, settings)
#    The API of this is largely the same as the native maven_jar rule,
#    except for the server attribute, which is not implemented. The optional
#    settings supports passing a custom Maven settings.xml to download the JAR.
# 2) maven_aar(name, artifact, sha1, settings)
#    The API of this rule is the same as maven_jar except that the artifact must
#    be the Maven coordinate of an AAR and it does not support the historical
#    repository and server attributes.
# 3) maven_dependency_plugin()
#    This rule downloads the maven-dependency-plugin used internally
#    for testing and the implementation for the fetching of artifacts.
#
# Maven coordinates are expected to be in this form:
# groupId:artifactId:version[:packaging][:classifier]
#
# Installation requirements prior to using this rule:
# 1) Maven binary: `mvn`
# 2) Maven plugin: `maven-dependency-plugin:2.10`
# Get it: $ mvn org.apache.maven.plugins:maven-dependency-plugin:2.10:get \
#    -Dartifact=org.apache.maven.plugins:maven-dependency-plugin:2.10 \
#    -Dmaven.repo.local=$HOME/.m2/repository # or specify your own local repository

"""Rules for retrieving Maven dependencies (experimental)"""

MAVEN_CENTRAL_URL = "https://repo1.maven.org/maven2"

# Binary dependencies needed for running the bash commands
DEPS = ["mvn", "openssl", "awk"]

MVN_PLUGIN = "org.apache.maven.plugins:maven-dependency-plugin:2.10"

def _execute(ctx, command):
    return ctx.execute(["bash", "-c", """
set -ex
%s""" % command])

# Fail fast
def _check_dependencies(ctx):
    for dep in DEPS:
        if ctx.which(dep) == None:
            fail("%s requires %s as a dependency. Please check your PATH." % (ctx.name, dep))

def _validate_attr(ctx):
    if hasattr(ctx.attr, "server") and (ctx.attr.server != None):
        fail("%s specifies a 'server' attribute which is currently not supported." % ctx.name)

def _artifact_dir(coordinates):
    return "/".join(coordinates.group_id.split(".") +
                    [coordinates.artifact_id, coordinates.version])

# Creates a struct containing the different parts of an artifact's FQN.
# If the fully_qualified_name does not specify a packaging and the rule does
# not set a default packaging then JAR is assumed.
def _create_coordinates(fully_qualified_name, packaging = "jar"):
    parts = fully_qualified_name.split(":")
    classifier = None

    if len(parts) == 3:
        group_id, artifact_id, version = parts

        # Updates the FQN with the default packaging so that the Maven plugin
        # downloads the correct artifact.
        fully_qualified_name = "%s:%s" % (fully_qualified_name, packaging)
    elif len(parts) == 4:
        group_id, artifact_id, version, packaging = parts
    elif len(parts) == 5:
        group_id, artifact_id, version, packaging, classifier = parts
    else:
        fail("Invalid fully qualified name for artifact: %s" % fully_qualified_name)

    return struct(
        fully_qualified_name = fully_qualified_name,
        group_id = group_id,
        artifact_id = artifact_id,
        packaging = packaging,
        classifier = classifier,
        version = version,
    )

# NOTE: Please use this method to define ALL paths that the maven_*
# rules use. Doing otherwise will lead to inconsistencies and/or errors.
#
# CONVENTION: *_path refers to files, *_dir refers to directories.
def _create_paths(ctx, coordinates):
    """Creates a struct that contains the paths to create the cache WORKSPACE"""

    # e.g. guava-18.0.jar
    artifact_filename = "%s-%s" % (
        coordinates.artifact_id,
        coordinates.version,
    )
    if coordinates.classifier:
        artifact_filename += "-" + coordinates.classifier
    artifact_filename += "." + coordinates.packaging
    sha1_filename = "%s.sha1" % artifact_filename

    # e.g. com/google/guava/guava/18.0
    relative_artifact_dir = _artifact_dir(coordinates)

    # The symlink to the actual artifact is stored in this dir, along with the
    # BUILD file. The dir has the same name as the packaging to support syntax
    # like @guava//jar and @google_play_services//aar.
    symlink_dir = coordinates.packaging

    m2 = ".m2"
    m2_repo = "/".join([m2, "repository"])  # .m2/repository

    return struct(
        artifact_filename = artifact_filename,
        sha1_filename = sha1_filename,
        symlink_dir = ctx.path(symlink_dir),

        # e.g. external/com_google_guava_guava/ \
        #        .m2/repository/com/google/guava/guava/18.0/guava-18.0.jar
        artifact_path = ctx.path("/".join([m2_repo, relative_artifact_dir, artifact_filename])),
        artifact_dir = ctx.path("/".join([m2_repo, relative_artifact_dir])),
        sha1_path = ctx.path("/".join([m2_repo, relative_artifact_dir, sha1_filename])),

        # e.g. external/com_google_guava_guava/jar/guava-18.0.jar
        symlink_artifact_path = ctx.path("/".join([symlink_dir, artifact_filename])),
    )

_maven_jar_build_file_template = """
# DO NOT EDIT: automatically generated BUILD file for maven_jar rule {rule_name}

java_import(
    name = 'jar',
    jars = ['{artifact_filename}'],
    deps = [
{deps_string}
    ],
    visibility = ['//visibility:public']
)

filegroup(
    name = 'file',
    srcs = ['{artifact_filename}'],
    visibility = ['//visibility:public']
)\n"""

_maven_aar_build_file_template = """
# DO NOT EDIT: automatically generated BUILD file for maven_aar rule {rule_name}

aar_import(
    name = 'aar',
    aar = '{artifact_filename}',
    deps = [
{deps_string}
    ],
    visibility = ['//visibility:public'],
)

filegroup(
    name = 'file',
    srcs = ['{artifact_filename}'],
    visibility = ['//visibility:public']
)\n"""

# Provides the syntax "@jar_name//jar" for dependencies
def _generate_build_file(ctx, template, paths):
    deps_string = "\n".join(["'%s'," % dep for dep in ctx.attr.deps])
    contents = template.format(
        rule_name = ctx.name,
        artifact_filename = paths.artifact_filename,
        deps_string = deps_string,
    )
    ctx.file("%s/BUILD" % paths.symlink_dir, contents, False)

# Constructs the maven command to retrieve the dependencies from remote
# repositories using the dependency plugin, and executes it.
def _mvn_download(ctx, paths, fully_qualified_name):
    # If a custom settings file exists, we'll use that. If not, Maven will use the default settings.
    mvn_flags = ""
    if hasattr(ctx.attr, "settings") and ctx.attr.settings != None:
        ctx.symlink(ctx.attr.settings, "settings.xml")
        mvn_flags += "-s %s " % "settings.xml"

    # dependency:get step. Downloads the artifact into the local repository.
    mvn_get = MVN_PLUGIN + ":get"
    mvn_artifact = "-Dartifact=%s" % fully_qualified_name
    mvn_transitive = "-Dtransitive=false"
    if hasattr(ctx.attr, "repository") and ctx.attr.repository != "":
        mvn_flags += "-Dmaven.repo.remote=%s " % ctx.attr.repository
    command = " ".join(["mvn", mvn_flags, mvn_get, mvn_transitive, mvn_artifact])
    exec_result = _execute(ctx, command)
    if exec_result.return_code != 0:
        fail("%s\n%s\nFailed to fetch Maven dependency" % (exec_result.stdout, exec_result.stderr))

    # dependency:copy step. Moves the artifact from the local repository into //external.
    mvn_copy = MVN_PLUGIN + ":copy"
    mvn_output_dir = "-DoutputDirectory=%s" % paths.artifact_dir
    command = " ".join(["mvn", mvn_flags, mvn_copy, mvn_artifact, mvn_output_dir])
    exec_result = _execute(ctx, command)
    if exec_result.return_code != 0:
        fail("%s\n%s\nFailed to fetch Maven dependency" % (exec_result.stdout, exec_result.stderr))

def _check_sha1(ctx, paths, sha1):
    actual_sha1 = _execute(ctx, "openssl sha1 %s | awk '{printf $2}'" % paths.artifact_path).stdout

    if sha1.lower() != actual_sha1.lower():
        fail(("{rule_name} has SHA-1 of {actual_sha1}, " +
              "does not match expected SHA-1 ({expected_sha1})").format(
            rule_name = ctx.name,
            expected_sha1 = sha1,
            actual_sha1 = actual_sha1,
        ))
    else:
        _execute(ctx, "echo %s %s > %s" % (sha1, paths.artifact_path, paths.sha1_path))

def _maven_artifact_impl(ctx, default_rule_packaging, build_file_template):
    # Ensure that we have all of the dependencies installed
    _check_dependencies(ctx)

    # Provide warnings and errors about attributes
    _validate_attr(ctx)

    # Create a struct to contain the different parts of the artifact FQN
    coordinates = _create_coordinates(ctx.attr.artifact, default_rule_packaging)

    # Create a struct to store the relative and absolute paths needed for this rule
    paths = _create_paths(ctx, coordinates)

    _generate_build_file(
        ctx = ctx,
        template = build_file_template,
        paths = paths,
    )

    if _execute(ctx, "mkdir -p %s" % paths.symlink_dir).return_code != 0:
        fail("%s: Failed to create dirs in execution root.\n" % ctx.name)

    # Download the artifact
    _mvn_download(
        ctx = ctx,
        paths = paths,
        fully_qualified_name = coordinates.fully_qualified_name,
    )

    if (ctx.attr.sha1 != ""):
        _check_sha1(
            ctx = ctx,
            paths = paths,
            sha1 = ctx.attr.sha1,
        )

    ctx.symlink(paths.artifact_path, paths.symlink_artifact_path)

_common_maven_rule_attrs = {
    "artifact": attr.string(
        default = "",
        mandatory = True,
    ),
    "sha1": attr.string(default = ""),
    "settings": attr.label(default = None),
    # Allow the user to specify deps for the generated java_import or aar_import
    # since maven_jar and maven_aar do not automatically pull in transitive
    # dependencies.
    "deps": attr.label_list(),
}

def _maven_jar_impl(ctx):
    _maven_artifact_impl(ctx, "jar", _maven_jar_build_file_template)

def _maven_aar_impl(ctx):
    _maven_artifact_impl(ctx, "aar", _maven_aar_build_file_template)

maven_jar = repository_rule(
    implementation = _maven_jar_impl,
    attrs = dict(_common_maven_rule_attrs.items() + {
        # Needed for compatibility reasons with the native maven_jar rule.
        "repository": attr.string(default = ""),
        "server": attr.label(default = None),
    }.items()),
    local = False,
)

maven_aar = repository_rule(
    implementation = _maven_aar_impl,
    attrs = _common_maven_rule_attrs,
    local = False,
)

def _maven_dependency_plugin_impl(ctx):
    _BUILD_FILE = """
# DO NOT EDIT: automatically generated BUILD file for maven_dependency_plugin

filegroup(
    name = 'files',
    srcs = glob(['**']),
    visibility = ['//visibility:public']
)
"""
    ctx.file("BUILD", _BUILD_FILE, False)

    _SETTINGS_XML = """
<!-- # DO NOT EDIT: automatically generated settings.xml for maven_dependency_plugin -->
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
    https://maven.apache.org/xsd/settings-1.0.0.xsd">
  <localRepository>{localRepository}</localRepository>
  <mirrors>
    <mirror>
      <id>central</id>
      <url>{mirror}</url>
      <mirrorOf>*,default</mirrorOf>
    </mirror>
  </mirrors>
</settings>
""".format(
        localRepository = ctx.path("repository"),
        mirror = MAVEN_CENTRAL_URL,
    )
    settings_path = ctx.path("settings.xml")
    ctx.file("%s" % settings_path, _SETTINGS_XML, False)

    # Download the plugin with transitive dependencies
    mvn_flags = "-s %s" % settings_path
    mvn_get = MVN_PLUGIN + ":get"
    mvn_artifact = "-Dartifact=%s" % MVN_PLUGIN
    command = " ".join(["mvn", mvn_flags, mvn_get, mvn_artifact])

    exec_result = _execute(ctx, command)
    if exec_result.return_code != 0:
        fail("%s\nFailed to fetch Maven dependency" % exec_result.stderr)

_maven_dependency_plugin = repository_rule(
    implementation = _maven_dependency_plugin_impl,
)

def maven_dependency_plugin():
    _maven_dependency_plugin(name = "m2")
